/*
 * WeatherManagerImpl
 *
 * Copyright 2008-2009 Centro Andaluz de Innovación y Tecnologías de la
 * Información y las Comunicaciones (CITIC). All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 *
 * Please contact CITIC - Edificio CITIC, C/ Marie Curie, 6 Parque
 * Tecnológico de Andalucía 29590 - Campanillas, Málaga, Spain
 * or visit www.citic.es, if you need additional information or
 * have any questions.
 */

package es.citic.puedes.weatherservice.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;

import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import es.citic.puedes.weatherservice.IWeatherManager;
import es.citic.puedes.weatherservice.WeatherException;
import es.citic.puedes.weatherservice.impl.bbdd.WeatherDataBase;
import es.citic.puedes.weatherservice.impl.umm.WeatherUserModelImpl;
import es.citic.puedes.weatherservice.model.Language;
import es.citic.puedes.weatherservice.model.Location;
import es.citic.puedes.weatherservice.model.Temperature;
import es.citic.puedes.weatherservice.model.Weather;
import es.citic.puedes.weatherservice.model.WeatherCondition;
import es.citic.puedes.weatherservice.model.Wind;
import es.citic.puedes.weatherservice.umm.IWeatherUserModel;
import es.citic.puedes.weatherservice.umm.WeatherPreferences;
import es.citic.puedes.weatherservice.utils.DataConfigManager;
import es.citic.puedes.weatherservice.utils.Utils;
import es.citic.puedes.weatherservice.utils.XmlManager;

public class WeatherManagerImpl implements IWeatherManager {
	private static Logger logger = LoggerFactory
			.getLogger(WeatherManagerImpl.class);

	private String key = "";
	public String domain = "";// "free.worldweatheronline.com";
	private WeatherDataBase weatherDB;
	private XmlManager xmlManager;

	public WeatherManagerImpl(String configFile) {
		this(configFile, new WeatherDataBase(
				new DataConfigManager(configFile).getValueFromConfigXml(
						"weathermanager", "constants", "persistence_unit")));
	}

	public WeatherManagerImpl(String configFile, WeatherDataBase weatherDataBase) {
		DataConfigManager dcm = new DataConfigManager(configFile);

		xmlManager = new XmlManager(dcm.getValueFromConfigXml("weathermanager",
				"config", "config_file"));
		weatherDB = weatherDataBase;
		domain = dcm
				.getValueFromConfigXml("weathermanager", "config", "domain");
		key = dcm.getValueFromConfigXml("weathermanager", "config", "key");
	}

	@Override
	public List<Location> getLocation(String location) throws WeatherException {
		logger.debug("Enter: getLocation (" + location + ")");

		if (location != null && !location.trim().isEmpty()) {
			List<Location> locationList = new ArrayList<Location>();
			location = getUnicodeLocation(location);

			// Get locations
			LocationResponse locationResponse = getDataLocation(location);

			// Map locations in Location list object
			LocationData[] locationArray = locationResponse.getSearch_api()
					.getResult();
			for (int i = 0; i < locationArray.length; i++) {
				Location newLocation = null;
				LocationData locationData = locationArray[i];
				Value[] areaName = locationData.getAreaName();
				Value[] region = locationData.getRegion();
				Value[] country = locationData.getCountry();
				if (region != null) {
					String nameLocation = areaName[0].getValue() + ", "
							+ region[0].getValue();
					newLocation = new Location(nameLocation,
							country[0].getValue(), locationData.getLatitude(),
							locationData.getLongitude(),
							locationData.getPopulation());
					locationList.add(newLocation);
				} else {
					newLocation = new Location(areaName[0].getValue(),
							country[0].getValue(), locationData.getLatitude(),
							locationData.getLongitude(),
							locationData.getPopulation());
					locationList.add(newLocation);
				}
			}
			logger.debug("Exit: getLocation()");
			return locationList;
		} else {
			throw new WeatherException("Location is empty");
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * es.citic.puedes.weatherservice.IWeatherManager#getWeather(java.lang.String
	 * , java.lang.String, int, es.citic.puedes.core.usermanager.IUser)
	 */
	@Override
	public WeatherCondition getWeather(String name, String country, int days,
			Language language, Integer user) throws WeatherException {
		logger.debug("Enter: getWeather(" + name + "," + country + "," + days
				+ "," + language + ")");

		WeatherCondition weatherCondition = null;
		if ((name == null || name.trim().isEmpty())
				|| (country == null || country.trim().isEmpty())) {
			throw new WeatherException("Location is empty");
		} else {
			if ((days >= 1) && (days <= 5)) {
				weatherCondition = new WeatherCondition();
				List<Weather> weather = new ArrayList<Weather>();

				// Get weather
				WeatherResponse response = getDataWeather(name, country, days);

				// Map info weather in WeatherCondition object
				RequestData[] requestData = response.getData().getRequest();
				weatherCondition.setCountry(getCountry(requestData[0]));
				weatherCondition.setLocation(getLocation(requestData[0]));

				ConditionData[] conditionData = response.getData()
						.getCurrent_condition();

				weatherCondition.setLastWeatherObservation(getCurrentCondition(
						conditionData[0], language));

				WeatherData[] weatherData = response.getData().getWeather();

				for (int i = 0; i < weatherData.length; i++) {
					logger.error(weatherData[i].getWeatherDesc()[0].getValue());
					weather.add(getWeather(weatherData[i], language));
				}

				weatherCondition.setWeatherCondition(weather);

			} else {
				throw new WeatherException(
						"Days are incorrect. Value must be between 1 to 5");
			}

			setPreferences(name, country, user);

		}
		logger.debug("Exit: getWeather()");
		return weatherCondition;
	}

	@Override
	public List<WeatherPreferences> getPreferences(Integer user,
			Language language) throws WeatherException {

		logger.debug("Enter: getPreferences(" + language + ")");

		IWeatherUserModel wum = null;
		List<WeatherPreferences> preferenceList = null;

		// Instances WeatherUserModelImpl
		wum = new WeatherUserModelImpl(user, weatherDB);

		// Get preferences
		preferenceList = wum.getPreferences(language);
		logger.debug("Exit: getPreferences()");

		return preferenceList;

	}
	
	/**
	 * 	
	 * @throws Throwable
	 */
	public void install() throws Throwable {
		logger.debug("Begins service installation..");
		weatherDB.install();
		logger.debug("Done service installation..");
	}

	// ------------------------------------------ PRIVATE METHODS
	// --------------------------------------------

	/**
	 * Translate String to unicode format String.
	 * 
	 * @param loc
	 *            Location name
	 * @return unicodeLocation Normalize location
	 * @see String
	 */
	private void setPreferences(String location, String country, Integer user)
			throws WeatherException {

		logger.debug("Enter: setPreferences(" + location + "," + country + ")");

		if ((location == null || location.trim().isEmpty())
				|| (country == null || country.trim().isEmpty())) {
			throw new WeatherException("Location is empty");
		} else {
			IWeatherUserModel wum = null;
			try {
				wum = new WeatherUserModelImpl(user, weatherDB);
			} catch (WeatherException e) {
				logger.error(e.getMessage());
			}

			// Set preferences
			WeatherPreferences preferences = new WeatherPreferences(user,
					location, country);
			((WeatherUserModelImpl)wum).setPreferences(preferences);

			// Save preferences
			wum.saveUserModel();
			logger.debug("Exit: setPreferences()");

			wum = null;
		}
	}
	
	private String getUnicodeLocation(String loc) {
		logger.debug("Enter: getUnicodeLocation(" + loc + ")");

		String location = loc.toLowerCase();
		location = location.replace(",", " ");
		location = location.trim();
		byte[] bytes = location.getBytes();
		for (int i = 0; i < bytes.length; i++) {
			byte b = bytes[i];
			b = getByte(b);
			bytes[i] = b;
		}

		String unicodeLocation = new String(bytes);
		unicodeLocation = getStringWithOneSpace(unicodeLocation);
		unicodeLocation = unicodeLocation.replace(" ", "_");

		logger.debug("Exit: getUnicodeLocation() -> " + unicodeLocation);
		return unicodeLocation;
	}

	/**
	 * Return a String with only one space between words.
	 * 
	 * @param name
	 *            String name
	 * @return result String with only one space between words
	 * @see String
	 */
	private String getStringWithOneSpace(String name) {
		logger.debug("Enter: getStringWithOneSpace(" + name + ")");

		String string = " ";
		String result = null;
		char space = string.charAt(0);
		char[] array = name.toCharArray();
		int count = 1;
		int times = 0;
		while (count < array.length) {
			if ((count > 0) && (count + 1 < array.length)) {
				if ((array[count] == space) && (array[count + 1] == space)) {
					for (int j = count; j < array.length - 1; j++) {
						array[j] = array[j + 1];

					}
					times = times + 1;
				} else {
					count = count + 1;
				}
			} else {
				count = count + 1;
			}
		}
		char[] newArray = new char[array.length - times];
		for (int j = 0; j < array.length - times; j++) {
			newArray[j] = array[j];
		}
		result = new String(newArray);

		logger.debug("Exit: getStringWithOneSpace() -> " + result);
		return result;
	}

	/**
	 * Translate String to unicode format String.
	 * 
	 * @param byte byte to map
	 * @return byte byte unicode
	 * @see byte
	 */
	private byte getByte(byte b) {
		logger.debug("Enter: getByte(" + b + ")");

		byte result = 0;
		switch (b) {
		case -31:
		case -32:
		case -28:
			result = 97;
			break;
		case -23:
		case -24:
		case -21:
			result = 101;
			break;
		case -19:
		case -20:
		case -17:
			result = 105;
			break;
		case -13:
		case -14:
		case -10:
			result = 111;
			break;
		case -6:
		case -7:
		case -4:
			result = 117;
			break;
		case -15:
			result = 110;
			break;
		case -25:
			result = 99;
			break;
		default:
			result = b;
			break;
		}
		logger.debug("Exit: getByte() -> " + result);
		return result;
	}

	/**
	 * Get locations from Free Weather API.
	 * 
	 * @param location
	 *            Name to search
	 * @return LocationResponse LocationResponse object with locations
	 * @throws WeatherException
	 *             If an exception occurred
	 * @see HttpURLConnection, InputStreamReader, StringBuffer, ObjectMapper,
	 *      LocationResponse
	 */
	private LocationResponse getDataLocation(String location)
			throws WeatherException {
		logger.debug("Enter: getDataLocation(" + location + ")");

		URL url;
		LocationResponse locationResponse = null;
		try {
			url = new URL("http://" + domain + "/feed/search.ashx?key=" + key
					+ "&query=" + location + "&format=json");
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.connect();
			InputStreamReader in = new InputStreamReader(
					(InputStream) conn.getContent());
			BufferedReader buff = new BufferedReader(in);
			StringBuffer text = new StringBuffer();
			// Read lines from response and pass to a String
			do {
				text.append(buff.readLine() + "\n");
			} while (buff.readLine() != null);
			String json = text.toString();
			ObjectMapper mapper = new ObjectMapper();
			// Map json string into LocationResponse object
			locationResponse = mapper.readValue(json, LocationResponse.class);
			if (locationResponse.getSearch_api() == null) {
				logger.error("Location doesn´t exist");
				throw new WeatherException("Location doesn´t exist");
			}
		} catch (IOException e) {
			logger.error(e.getMessage(), e);
			throw new WeatherException(e.getMessage(), e.getCause());
		}
		logger.debug("Exit: getDataLocation() -> " + locationResponse);

		return locationResponse;
	}

	/**
	 * Get weather from Free Weather API.
	 * 
	 * @param name
	 *            City or Town name
	 * @param country
	 *            Country name
	 * @param days
	 *            Number of days for prediction
	 * @return WeatherResponse Object with weather information
	 * @throws WeatherException
	 *             If an exception occurred
	 * @see HttpURLConnection, InputStreamReader, StringBuffer, ObjectMapper,
	 *      WeatherResponse
	 */
	private WeatherResponse getDataWeather(String name, String country, int days)
			throws WeatherException {
		URL url = null;
		WeatherResponse weatherResponse = null;

		logger.debug("Enter: getDataWeather (" + name + "," + country + ","
				+ days + ")");

		name = name.trim();
		country = country.trim();
		name = getStringWithOneSpace(name);
		country = getStringWithOneSpace(country);
		name = name.replace(", ", ",");
		country = country.replace(", ", ",");
		name = name.replace(" ", "_");
		country = country.replace(" ", "_");
		try {
			url = new URL("http://" + domain + "/feed/weather.ashx?q=" + name
					+ "," + country + "&format=json&num_of_days=" + days
					+ "&key=" + key);
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.connect();
			InputStreamReader in = new InputStreamReader(
					(InputStream) conn.getContent());
			BufferedReader buff = new BufferedReader(in);
			StringBuffer text = new StringBuffer();
			// Read lines from response and pass to a String
			do {
				text.append(buff.readLine() + "\n");
			} while (buff.readLine() != null);
			String json = text.toString();
			ObjectMapper mapper = new ObjectMapper();

			// Map json string into WeatherResponse object
			weatherResponse = mapper.readValue(json, WeatherResponse.class);
		} catch (IOException e) {
			logger.error(e.getMessage(), e);
			throw new WeatherException("Location and/or country are wrong");
		}
		logger.debug("Exit: getDataWeather()");

		return weatherResponse;
	}

	/**
	 * Get icon object (description about icon) by description code and current
	 * time prediction.
	 * 
	 * @param description
	 *            Description code get as response
	 * @return Description Description object
	 * @throws WeatherException
	 *             If an exception occurred
	 * @see Description, Calendar
	 */

	// modification cause: some of the descriptions were assigned to incorrect
	// icons
	// must be added an icon for 'Blizzard'. Now is related with 'cloudy'
	private String getIcon(String description) throws WeatherException {
		logger.debug("Enter: getDescription (String description)"
				+ description.toLowerCase());
		String icon = null;

		if (description.toLowerCase().contains("rain")
				|| (description.toLowerCase().contains("drizzle"))) {
			icon = "rain";
		} else if (description.toLowerCase().contains("snow")
				|| (description.toLowerCase().contains("sleet") || (description
						.toLowerCase().contains("pellets")))) {
			icon = "snow";
		} else if (description.toLowerCase().contains("cloud")) {
			icon = "cloudy";
		} else if (description.toLowerCase().contains("storm")
				|| (description.toLowerCase().contains("thundery"))) {
			icon = "storm";
		} else if (description.toLowerCase().contains("sunny")
				|| (description.toLowerCase().contains("clear"))) {
			icon = "sunny";
		} else if ((description.toLowerCase().contains("mist"))
				|| (description.toLowerCase().contains("fog"))
				|| (description.toLowerCase().contains("over"))) {
			icon = "cloudy";
		} else {
			icon = "cloudy";
		}
		logger.debug("Exit: getDescription (String description)");

		return icon;
	}

	/**
	 * Get country from RequestData.
	 * 
	 * @param request
	 *            RequestData to get information
	 * @return country Country name
	 * @see String
	 */
	private String getCountry(RequestData request) {
		logger.debug("Enter: getCountry (RequestData request)");
		if (request != null) {
			// Separate Name and Country locatio from the same String
			String query = request.getQuery();
			String[] names = query.split(",");
			logger.debug("Exit: getCountry (RequestData request)");

			// Return Country name location
			return names[1];
		} else {
			return null;
		}
	}

	/**
	 * Get location name from RequestData.
	 * 
	 * @param request
	 *            RequestData object to get information
	 * @return location Location name
	 * @see String
	 */
	private String getLocation(RequestData request) {
		logger.debug("Enter: getLocation (RequestData request)");

		if (request != null) {
			// Separate Name and Country location from the same String
			String query = request.getQuery();
			String[] names = query.split(",");
			logger.debug("Exit: getLocation (RequestData request)");

			// Return name location
			return names[0];
		} else {
			return null;
		}
	}

	/**
	 * Get CurrentCondition weather from ConditionData object.
	 * 
	 * @param condition
	 *            ConditionData object to get information
	 * @param language
	 *            language to get the current condition
	 * @return Weather CurrentCondition weather with information
	 * @throws WeatherException
	 *             If exception occurred
	 * @see Temperature, Wind, Calendar, Description, Weather, Value
	 */

	// modification cause: set the parameter 'language' for getting the
	// prediction in the language desired
	private Weather getCurrentCondition(ConditionData condition,
			Language language) throws WeatherException {
		logger.debug("Enter: getCurrentCondition(ConditionData condition)");

		Weather lastWeatherObservation = null;
		try {
			if (condition != null) {
				Calendar timeCondition = GregorianCalendar.getInstance();
				SimpleDateFormat s = new SimpleDateFormat("HH:mm aa");
				Date d = s.parse(condition.getObservation_time());

				Calendar calendar = Calendar.getInstance();
				calendar.setTime(d);
				timeCondition.set(Calendar.HOUR, calendar.get(Calendar.HOUR));
				timeCondition.set(Calendar.MINUTE,
						calendar.get(Calendar.MINUTE));

				if (condition.getObservation_time().contains("AM")) {
					timeCondition.set(Calendar.AM_PM, 0);
				} else {
					timeCondition.set(Calendar.AM_PM, 1);
				}

				// Create Temperature object
				Temperature temperature = new Temperature(
						condition.getTemp_C(), condition.getTemp_C(),
						condition.getTemp_F(), condition.getTemp_F());

				// Create Wind object

				// modification cause: gets the wind direction from the
				// weather_config.xml using the windDirection code
				String windDirectionValue = xmlManager.getValueFromXmlDocument(
						"winddirections", condition.getWinddir16Point(),
						language.getReadableValue());

				logger.debug("wind direction from xml: " + windDirectionValue);
				Wind wind = new Wind(condition.getWindspeedMiles(),
						condition.getWindspeedKmph(), windDirectionValue);

				// Create description object

				// modification cause: gets the description from the
				// weather_config.xml using the description code
				String descriptionValue = xmlManager.getValueFromXmlDocument(
						"weatherdescriptions",
						Integer.toString(condition.getWeatherCode()),
						language.getReadableValue());
				// condition.getWeatherCode()
				logger.debug("weather description from xml: "
						+ descriptionValue);
				Value[] description = condition.getWeatherDesc();

				// Get icon URL
				condition.getWeatherIconUrl();

				// Create Weather object with last current condition
				lastWeatherObservation = new Weather();
				lastWeatherObservation.setDate(timeCondition.getTime()
						.toString());

				// lastWeatherObservation.setDescription(description[0].getValue());

				// modification cause: set the description value from the xml
				// file
				lastWeatherObservation.setDescription(descriptionValue);

				lastWeatherObservation.setIconURL(getIcon(description[0]
						.getValue()));
				lastWeatherObservation
						.setPrecipitation(condition.getPrecipMM());
				lastWeatherObservation.setTemperature(temperature);
				lastWeatherObservation.setWind(wind);
			}
		} catch (ParseException e) {
			logger.error(e.getMessage());
		} catch (WeatherException e) {
			logger.error(e.getMessage());
			throw new WeatherException(e.getMessage(), e.getCause());
		}
		logger.debug("Exit: getCurrentCondition(ConditionData condition)");

		return lastWeatherObservation;
	}

	/**
	 * Get Weather from WeatherData object.
	 * 
	 * @param WeatherData
	 *            Weather object to get information
	 * @param language
	 *            language to get the current condition
	 * @return weatherData WeatherData object with information
	 * @throws WeatherException
	 *             If exception occurred
	 * @see Temperature, Wind, Calendar, Description, Weather, Value
	 */

	// modification cause: set the parameter 'language' for getting the
	// prediction in the language desired
	private Weather getWeather(WeatherData weatherData, Language language)
			throws WeatherException {
		logger.debug("Enter: getWeather(WeatherData weatherData)");

		Weather weather = null;
		try {
			if (weatherData != null) {

				// Create Temperature object
				Temperature temperature = new Temperature(
						weatherData.getTempMaxC(), weatherData.getTempMinC(),
						weatherData.getTempMaxF(), weatherData.getTempMinF());

				// Create Wind object
				String windDirectionValue = xmlManager.getValueFromXmlDocument(
						"winddirections", weatherData.getWinddirection(),
						language.getReadableValue());
				logger.debug("wind direction value from xml: "
						+ windDirectionValue);
				Wind wind = new Wind(weatherData.getWindspeedMiles(),
						weatherData.getWindspeedKmph(), windDirectionValue);

				SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd");
				Date date;

				date = dateformat.parse(weatherData.getDate());
				Calendar calendar = Calendar.getInstance();
				calendar.setTime(date);

				// Create description object
				String descriptionValue = xmlManager.getValueFromXmlDocument(
						"weatherdescriptions",
						Integer.toString(weatherData.getWeatherCode()),
						language.getReadableValue());
				logger.debug("weather description value from xml: "
						+ descriptionValue);
				Value[] weatherDescription = weatherData.getWeatherDesc();

				// Create weather object
				String formatDate = Utils.convertDateToStringLanguageDate(date,
						language);
				weather = new Weather(temperature, wind,
						getIcon(weatherDescription[0].getValue()),
						descriptionValue, weatherData.getPrecipMM(), formatDate);

			}

		} catch (ParseException e) {
			logger.error(e.getMessage());
			throw new WeatherException(e.getMessage(), e.getCause());
		}
		logger.debug("Exit: getWeather(WeatherData weatherData)");
		return weather;
	}

}
